TypeScript란 무엇인가?
1.1 한 줄 정의
TypeScript는 Microsoft가 개발한 오픈소스 프로그래밍 언어로, JavaScript에 정적 타입 시스템을 추가한 상위 집합(Superset)입니다. 모든 유효한 JavaScript 코드는 그대로 유효한 TypeScript 코드이며, TypeScript 컴파일러(tsc)가 타입을 검사한 뒤 순수 JavaScript로 변환합니다. 최종적으로 브라우저나 Node.js에서 실행되는 것은 JavaScript입니다.
1.2 탄생 배경
JavaScript는 1995년 넷스케이프에서 10일 만에 만들어진 스크립트 언어였습니다. 원래 웹 페이지에 간단한 상호작용을 넣기 위한 용도였으나, Node.js(2009), React(2013), 그리고 수많은 프레임워크의 등장으로 JavaScript는 프론트엔드, 백엔드, 모바일, 데스크톱 앱까지 아우르는 범용 언어가 되었습니다. 그러나 JavaScript에는 근본적인 한계가 있었습니다.
JavaScript는 동적 타입(Dynamic Typing) 언어입니다. 변수에 아무 값이나 넣을 수 있고, 함수에 어떤 인자든 전달할 수 있습니다. 작은 프로젝트에서는 문제가 없지만, 코드가 수만~수십만 줄로 늘어나면 "이 변수에 무엇이 들어있는지", "이 함수가 무엇을 반환하는지" 파악하기가 극히 어려워집니다. 런타임에서야 비로소 에러가 터지고, 그 에러는 사용자가 마주하게 됩니다.
Microsoft의 Anders Hejlsberg(C#의 아버지)가 이끄는 팀은 2012년에 TypeScript를 공개했습니다. 핵심 아이디어는 단순했습니다. JavaScript의 자유로움은 유지하면서, 개발 단계에서 타입을 검사하여 버그를 미리 잡자는 것입니다.
1.3 TypeScript의 핵심 철학
TypeScript 설계 원칙 문서(Design Goals)에는 몇 가지 중요한 철학이 담겨 있습니다. 첫째, JavaScript와의 완벽한 호환성입니다. TypeScript는 새로운 런타임을 만들지 않습니다. 기존 JavaScript 생태계(npm, 브라우저 API, Node.js API)를 그대로 사용합니다. 둘째, 점진적 도입이 가능합니다. 기존 .js 파일을 .ts로 바꾸는 것만으로도 시작할 수 있고, 타입을 엄격하게 쓸지, 느슨하게 쓸지 선택할 수 있습니다. 셋째, 타입은 런타임에 영향을 주지 않습니다. 컴파일 후 모든 타입 정보는 제거되므로, 성능 오버헤드가 없습니다.
1.4 TypeScript ≠ 새로운 언어
흔한 오해 중 하나는 "TypeScript를 배우려면 JavaScript를 완전히 마스터한 뒤에 시작해야 한다"는 것입니다. 실제로는 TypeScript를 배우면서 JavaScript를 더 잘 이해하게 되는 경우가 많습니다. TypeScript의 타입 시스템은 JavaScript의 런타임 동작을 정확히 모델링하려고 노력하기 때문입니다. 예를 들어, typeof 연산자, 프로토타입 체인, 클로저 등 JavaScript의 핵심 개념을 타입 수준에서 다시 만나게 됩니다.
왜 TypeScript를 사용해야 하는가
2.1 개발 단계에서 버그를 잡는다
JavaScript에서 가장 흔한 버그 유형은 "TypeError: Cannot read properties of undefined"입니다. TypeScript는 이런 에러를 코드 작성 시점에 감지합니다. 실행해 보기 전에, 에디터에서 빨간 밑줄이 그어지면서 문제를 알려줍니다. 이것이 TypeScript의 가장 핵심적인 가치입니다.
// JavaScript: 아무런 경고 없이 실행됨
function greet(user) {
return "Hello, " + user.name.toUpperCase();
}
greet(undefined);
// 💥 런타임 에러: Cannot read properties of undefined (reading 'name')
// TypeScript: 코드 작성 시점에 에러 감지
interface User {
name: string;
email: string;
}
function greet(user: User): string {
return "Hello, " + user.name.toUpperCase();
}
greet(undefined);
// ❌ 컴파일 에러: Argument of type 'undefined' is not assignable to parameter of type 'User'
2.2 에디터 지원(DX)이 극적으로 좋아진다
TypeScript를 사용하면 VS Code 등의 에디터에서 자동완성, 리팩토링, 정의로 이동(Go to Definition), 참조 찾기(Find References)가 정확하게 작동합니다. JavaScript에서도 에디터가 타입을 추론하려 노력하지만, 정보가 부족해서 부정확한 경우가 많습니다. TypeScript는 명시적인 타입 정보를 갖고 있으므로 에디터가 정확한 도움을 줄 수 있습니다. 이것은 생산성에 엄청난 차이를 만듭니다.
2.3 코드가 곧 문서가 된다
함수의 매개변수에 타입이 적혀 있으면, 별도의 문서 없이도 "이 함수가 무엇을 받고 무엇을 반환하는지" 명확히 알 수 있습니다. 팀원이 작성한 코드를 처음 읽을 때, JSDoc 주석을 찾아헤매는 대신 타입 시그니처만 보면 됩니다. 인터페이스와 타입 정의는 API 사양서 역할을 합니다.
2.4 대규모 리팩토링이 안전해진다
프로젝트에서 함수 이름을 바꾸거나, 객체 구조를 변경하거나, API 응답 형식을 수정해야 할 때가 있습니다. JavaScript에서는 이런 변경이 어디에 영향을 주는지 전역 검색(grep)에 의존해야 하고, 빠뜨린 부분은 런타임에서야 발견됩니다. TypeScript에서는 타입을 바꾸는 순간 영향받는 모든 곳에 컴파일 에러가 표시됩니다. 에러를 하나씩 고치면 리팩토링이 완료됩니다.
2.5 업계 표준이 되었다
2024년 Stack Overflow 설문에서 TypeScript는 "가장 사랑받는 언어" 상위권에 위치했고, 새로 시작하는 프로젝트의 대다수가 TypeScript를 기본으로 채택합니다. React, Next.js, Angular, Vue 3, Svelte, Nest.js, Prisma, tRPC 등 주요 프레임워크와 라이브러리가 TypeScript를 공식 지원하거나 TypeScript로 작성되어 있습니다. npm 패키지의 타입 정의 저장소인 DefinitelyTyped에는 12,000개 이상의 패키지 타입이 등록되어 있습니다.
| 항목 | JavaScript | TypeScript |
|---|---|---|
| 타입 시스템 | 동적 (런타임) | 정적 (컴파일 타임) + 동적 |
| 에러 발견 시점 | 실행 중 | 코드 작성 중 |
| 자동완성 정확도 | 보통 | 매우 높음 |
| 러닝커브 | 낮음 | 약간 높음 (타입 학습 필요) |
| 빌드 단계 | 불필요 (또는 번들러만) | 컴파일 필요 (tsc, esbuild 등) |
| 런타임 성능 | 동일 | 동일 (타입은 컴파일 시 제거) |
strict 옵션을 끄면 느슨한 검사부터 시작하여 점진적으로 엄격도를 높일 수 있습니다.
환경 설정
3.1 Node.js 설치
TypeScript 컴파일러는 Node.js 위에서 동작합니다. Node.js 공식 사이트(nodejs.org)에서 LTS 버전을 설치하면 npm(패키지 관리자)도 함께 설치됩니다.
# Node.js 버전 확인
node --version # v20.x.x 이상 권장
npm --version # 10.x.x
3.2 TypeScript 설치
# 글로벌 설치 (어디서든 tsc 명령어 사용 가능)
npm install -g typescript
# 프로젝트 로컬 설치 (권장 - 팀원과 버전 통일)
npm install -D typescript
# 버전 확인
tsc --version # Version 5.x.x 또는 6.x.x
3.3 프로젝트 초기화
# 새 프로젝트 디렉터리 생성
mkdir my-ts-project && cd my-ts-project
# package.json 생성
npm init -y
# TypeScript 설치
npm install -D typescript
# tsconfig.json 생성 (TypeScript 설정 파일)
npx tsc --init
3.4 첫 번째 TypeScript 파일
const message: string = "Hello, TypeScript!";
console.log(message);
function add(a: number, b: number): number {
return a + b;
}
console.log(add(3, 5)); // 8
# 컴파일: .ts → .js 변환
npx tsc hello.ts
# 실행
node hello.js
# 또는 ts-node로 한 번에 (컴파일 + 실행)
npx ts-node hello.ts
# 또는 tsx로 한 번에 (더 빠름, ESM 지원)
npx tsx hello.ts
3.5 추천 개발 도구
VS Code는 TypeScript 개발에 가장 적합한 에디터입니다. TypeScript 언어 서비스가 내장되어 있어 별도 확장 없이도 타입 검사, 자동완성, 리팩토링이 동작합니다. 추가로 유용한 도구로는 실시간 컴파일을 해주는 ts-node/tsx, 테스트 프레임워크인 Vitest, 코드 품질 검사 도구인 ESLint, 그리고 빠른 빌드를 위한 esbuild/swc 등이 있습니다.
3.6 온라인 플레이그라운드
설치 없이 TypeScript를 바로 체험하고 싶다면 공식 TypeScript Playground(typescriptlang.org/play)를 이용하세요. 코드를 작성하면 실시간으로 타입 검사 결과와 컴파일된 JavaScript를 볼 수 있습니다. 다양한 tsconfig 옵션도 UI에서 바로 변경하며 테스트할 수 있어서, 학습에 매우 유용합니다.
node --experimental-strip-types hello.ts처럼 별도 도구 없이 바로 실행할 수 있습니다. Deno와 Bun은 이미 TypeScript를 네이티브로 지원합니다.
기본 타입 (Primitive Types)
4.1 타입 어노테이션의 기본
TypeScript에서 변수에 타입을 지정하는 방법은 간단합니다. 변수 이름 뒤에 콜론(:)과 타입 이름을 씁니다. 이것을 타입 어노테이션(Type Annotation)이라 합니다. TypeScript는 또한 초기값으로부터 타입을 자동으로 추론하는 타입 추론(Type Inference) 기능이 있어서, 많은 경우 타입을 명시적으로 적지 않아도 됩니다.
// 타입 어노테이션 (명시적)
let username: string = "김민수";
let age: number = 28;
let isActive: boolean = true;
// 타입 추론 (TypeScript가 자동으로 타입을 알아냄)
let city = "서울"; // string으로 추론
let score = 95; // number로 추론
let isDone = false; // boolean으로 추론
// 타입이 맞지 않으면 에러!
username = 42;
// ❌ Type 'number' is not assignable to type 'string'
4.2 기본 타입 총정리
| 타입 | 설명 | 예시 |
|---|---|---|
string | 문자열 | "hello", 'world', `템플릿` |
number | 정수, 실수, NaN, Infinity 등 모든 숫자 | 42, 3.14, 0xff |
boolean | 참/거짓 | true, false |
null | 의도적으로 "값 없음" | null |
undefined | 아직 값이 할당되지 않음 | undefined |
bigint | 매우 큰 정수 (ES2020+) | 9007199254740991n |
symbol | 고유하고 변경 불가능한 식별자 | Symbol("id") |
any | 모든 타입 허용 (타입 검사 비활성화) | 가급적 사용 지양 |
unknown | 모든 값 가능하나, 사용 전 타입 확인 필수 | any의 안전한 대안 |
void | 반환값이 없는 함수의 반환 타입 | function log(): void |
never | 절대 발생하지 않는 값 (에러, 무한루프) | function fail(): never |
4.3 배열과 튜플
// 배열: 같은 타입의 요소 목록
let numbers: number[] = [1, 2, 3, 4, 5];
let names: string[] = ["Alice", "Bob"];
let mixed: (string | number)[] = ["hello", 42];
// Array<T> 제네릭 문법 (동일한 의미)
let items: Array<string> = ["a", "b", "c"];
// 튜플(Tuple): 고정 길이, 각 위치의 타입이 정해진 배열
let person: [string, number] = ["김민수", 28];
let coordinate: [number, number] = [37.5665, 126.978];
// 옵셔널 요소가 있는 튜플
let flexible: [string, number?] = ["hello"]; // 두 번째 요소 생략 가능
// 나머지 요소가 있는 튜플
let row: [string, ...number[]] = ["scores", 90, 85, 92];
4.4 any vs unknown
any와 unknown은 모두 "모든 값을 받을 수 있는 타입"이지만, 사용 방식이 완전히 다릅니다. any는 타입 검사를 완전히 끄는 것과 같습니다. 어떤 속성이든 접근 가능하고, 어떤 함수든 호출 가능합니다. 반면 unknown은 값을 받는 것은 허용하지만, 사용하기 전에 반드시 타입을 확인해야 합니다. 안전한 코드를 위해서는 any 대신 unknown을 사용하는 것이 좋습니다.
// any: 타입 검사 완전 비활성화 (위험!)
let danger: any = "hello";
danger.nonExistentMethod(); // 에러 없음! → 런타임에서 터짐
danger.foo.bar.baz; // 에러 없음! → 런타임에서 터짐
// unknown: 안전한 any
let safe: unknown = "hello";
safe.toUpperCase(); // ❌ 에러! unknown에서는 바로 사용 불가
// typeof로 타입 확인 후 사용 (타입 좁히기)
if (typeof safe === "string") {
safe.toUpperCase(); // ✅ 이제 string으로 확정, 사용 가능!
}
4.5 리터럴 타입
TypeScript에서는 특정 값 자체를 타입으로 사용할 수 있습니다. 이를 리터럴 타입(Literal Type)이라 합니다. const로 선언한 변수는 자동으로 리터럴 타입으로 추론됩니다.
// let은 넓은 타입으로 추론
let changeable = "hello"; // 타입: string
// const는 리터럴 타입으로 추론
const fixed = "hello"; // 타입: "hello" (문자열 리터럴)
// 리터럴 타입을 활용한 제한
type Direction = "up" | "down" | "left" | "right";
type DiceRoll = 1 | 2 | 3 | 4 | 5 | 6;
type Toggle = true | false;
function move(direction: Direction) { /* ... */ }
move("up"); // ✅
move("north"); // ❌ '"north"'는 'Direction'에 할당할 수 없습니다
any는 전염성이 있습니다. any 타입의 변수와 연산하면 결과도 any가 됩니다. tsconfig에서 "noImplicitAny": true를 설정하면, 타입 추론이 불가능할 때 any로 대체되는 것을 에러로 잡아줍니다.
함수와 타입
5.1 매개변수와 반환 타입
함수는 TypeScript에서 가장 많이 타입을 지정하는 대상입니다. 매개변수 타입은 반드시 명시해야 하며(strict 모드), 반환 타입은 대부분 자동 추론되지만 명시하는 것이 가독성과 안전성 측면에서 좋습니다.
// 기본적인 함수 타입 지정
function add(a: number, b: number): number {
return a + b;
}
// 화살표 함수
const multiply = (a: number, b: number): number => a * b;
// 반환값이 없는 함수 → void
function logMessage(msg: string): void {
console.log(msg);
}
// 절대 반환하지 않는 함수 → never
function throwError(message: string): never {
throw new Error(message);
}
5.2 선택적 매개변수와 기본값
// 선택적 매개변수: 이름 뒤에 ? (항상 필수 매개변수 뒤에 위치)
function greet(name: string, greeting?: string): string {
return `${greeting ?? "안녕하세요"}, ${name}님!`;
}
greet("민수"); // "안녕하세요, 민수님!"
greet("민수", "환영합니다"); // "환영합니다, 민수님!"
// 기본값 매개변수
function createUser(name: string, role: string = "user"): string {
return `${name} (${role})`;
}
createUser("Alice"); // "Alice (user)"
createUser("Bob", "admin"); // "Bob (admin)"
5.3 나머지 매개변수 (Rest Parameters)
function sum(...nums: number[]): number {
return nums.reduce((acc, cur) => acc + cur, 0);
}
sum(1, 2, 3); // 6
sum(10, 20, 30, 40); // 100
5.4 함수 타입 표현식
함수 자체를 값으로 다룰 때, 함수의 타입을 별도로 정의할 수 있습니다. 콜백 함수의 타입을 지정할 때 특히 유용합니다.
// 함수 타입 표현식
type MathOperation = (a: number, b: number) => number;
const add: MathOperation = (a, b) => a + b;
const subtract: MathOperation = (a, b) => a - b;
// 콜백 함수 타입 지정
function calculate(a: number, b: number, operation: MathOperation): number {
return operation(a, b);
}
calculate(10, 5, add); // 15
calculate(10, 5, subtract); // 5
5.5 함수 오버로드
같은 함수 이름으로 다양한 매개변수 조합을 지원해야 할 때 함수 오버로드를 사용합니다. 오버로드 시그니처(선언)와 구현 시그니처(본문)를 분리합니다.
// 오버로드 시그니처
function format(value: string): string;
function format(value: number, decimals: number): string;
// 구현 시그니처 (외부에서 직접 호출할 수 없음)
function format(value: string | number, decimals?: number): string {
if (typeof value === "string") {
return value.trim();
}
return value.toFixed(decimals);
}
format(" hello "); // "hello"
format(3.14159, 2); // "3.14"
인터페이스 (Interface)
6.1 인터페이스란?
인터페이스는 객체의 구조(형태)를 정의하는 계약서입니다. "이 객체는 어떤 속성을 가지고 있고, 각 속성은 어떤 타입인지"를 명시합니다. 인터페이스는 런타임에 존재하지 않으며, 오직 컴파일 타임에 타입 검사를 위해서만 사용됩니다. TypeScript의 타입 시스템은 구조적 타이핑(Structural Typing)을 사용합니다. 즉, 이름이 아니라 구조(속성과 타입)가 같으면 같은 타입으로 간주합니다.
// 인터페이스 정의
interface User {
id: number;
name: string;
email: string;
age?: number; // 선택적 속성
readonly createdAt: Date; // 읽기 전용 (수정 불가)
}
// 인터페이스를 타입으로 사용
const user: User = {
id: 1,
name: "김민수",
email: "minsu@mail.com",
createdAt: new Date()
};
user.createdAt = new Date(); // ❌ readonly 속성은 수정 불가
6.2 인터페이스 확장 (extends)
인터페이스는 다른 인터페이스를 상속받아 확장할 수 있습니다. 이를 통해 코드 재사용성을 높이고, 관련 있는 타입들 사이의 계층 구조를 만들 수 있습니다.
interface Animal {
name: string;
age: number;
}
interface Dog extends Animal {
breed: string;
bark(): void;
}
interface ServiceDog extends Dog {
certification: string;
handler: string;
}
// 다중 상속도 가능!
interface Serializable {
toJSON(): string;
}
interface Loggable {
log(): void;
}
interface TrackedUser extends User, Serializable, Loggable {
lastLogin: Date;
}
6.3 인터페이스 병합 (Declaration Merging)
같은 이름의 인터페이스를 여러 번 선언하면 자동으로 합쳐집니다. 이 특성은 외부 라이브러리의 타입을 확장할 때 특히 유용합니다. 타입 별칭(type)에서는 이것이 불가능합니다.
interface Window {
title: string;
}
interface Window {
appVersion: string;
}
// 결과적으로 Window는 { title: string; appVersion: string; }
const w: Window = {
title: "My App",
appVersion: "1.0.0"
};
6.4 인덱스 시그니처
객체의 속성 이름을 미리 알 수 없지만, 값의 타입은 알고 있을 때 인덱스 시그니처를 사용합니다.
interface Dictionary {
[key: string]: string;
}
const colors: Dictionary = {
red: "#ff0000",
blue: "#0000ff",
green: "#00ff00",
// 어떤 키든 추가 가능, 단 값은 반드시 string
};
interface Scores {
[subject: string]: number;
// 고정 속성과 인덱스 시그니처 병합 가능
total: number; // 단, 인덱스 시그니처의 타입과 호환되어야 함
}
타입 별칭과 유니온/인터섹션
7.1 타입 별칭 (Type Alias)
type 키워드로 타입에 이름을 붙입니다. 인터페이스와 비슷하지만 더 넓은 범위의 타입을 표현할 수 있습니다. 원시 타입, 유니온, 튜플, 함수 등 모든 타입에 이름을 붙일 수 있습니다.
// 원시 타입 별칭
type ID = string | number;
type Email = string;
// 객체 타입 별칭
type Point = {
x: number;
y: number;
};
// 함수 타입 별칭
type Formatter = (input: string) => string;
// 튜플 타입 별칭
type Coordinate = [number, number];
// 리터럴 유니온 타입 별칭
type Status = "pending" | "active" | "closed";
7.2 interface vs type — 언제 무엇을 쓸까?
| 기능 | interface | type |
|---|---|---|
| 객체 구조 정의 | ✅ | ✅ |
| extends로 확장 | ✅ | ✅ (& 인터섹션으로) |
| 선언 병합 | ✅ | ❌ |
| 유니온 타입 | ❌ | ✅ |
| 원시/튜플/함수 별칭 | ❌ | ✅ |
| computed properties | ❌ | ✅ (mapped types) |
실무에서의 가이드라인은 이렇습니다. 객체의 형태를 정의할 때는 interface를, 유니온이나 복잡한 타입 조합에는 type을 사용하는 것이 일반적입니다. 다만 팀 내에서 일관성을 유지하는 것이 더 중요합니다.
7.3 유니온 타입 (Union Type)
여러 타입 중 하나일 수 있는 값을 표현합니다. | (파이프) 기호로 연결합니다.
type StringOrNumber = string | number;
function printId(id: StringOrNumber) {
// id가 string인지 number인지 모르는 상태
// 두 타입에 공통으로 존재하는 메서드만 사용 가능
console.log(id.toString()); // ✅ 두 타입 모두 toString() 보유
// 타입 좁히기 후 각 타입에 맞는 메서드 사용 가능
if (typeof id === "string") {
console.log(id.toUpperCase()); // ✅ string 확정
} else {
console.log(id.toFixed(2)); // ✅ number 확정
}
}
7.4 판별 유니온 (Discriminated Union)
가장 강력하고 실용적인 TypeScript 패턴 중 하나입니다. 공통된 리터럴 속성(판별자)을 기준으로 유니온의 각 타입을 구분합니다.
interface Circle {
kind: "circle"; // ← 판별자 (discriminant)
radius: number;
}
interface Rectangle {
kind: "rectangle";
width: number;
height: number;
}
interface Triangle {
kind: "triangle";
base: number;
height: number;
}
type Shape = Circle | Rectangle | Triangle;
function getArea(shape: Shape): number {
switch (shape.kind) {
case "circle":
return Math.PI * shape.radius ** 2;
case "rectangle":
return shape.width * shape.height;
case "triangle":
return (shape.base * shape.height) / 2;
}
}
7.5 인터섹션 타입 (Intersection Type)
여러 타입을 합쳐서 모든 속성을 가진 타입을 만듭니다. & 기호로 연결합니다.
type HasName = { name: string };
type HasAge = { age: number };
type HasEmail = { email: string };
// 인터섹션: 세 타입의 모든 속성을 가져야 함
type Person = HasName & HasAge & HasEmail;
const person: Person = {
name: "김민수",
age: 28,
email: "minsu@mail.com"
// 하나라도 빠지면 에러!
};
제네릭 (Generics)
8.1 제네릭이란?
제네릭은 "타입을 매개변수로 받는" 기능입니다. 함수, 인터페이스, 클래스 등이 특정 타입에 고정되지 않고, 사용할 때 타입을 지정할 수 있게 합니다. 코드의 재사용성과 타입 안전성을 동시에 달성하는 TypeScript의 가장 강력한 기능입니다.
비유하자면, 제네릭은 "상자"와 같습니다. 상자 자체는 어떤 물건이든 담을 수 있지만, 한번 물건의 종류를 정하면(예: 과일 상자) 그 상자에는 과일만 넣을 수 있습니다.
// ❌ any를 쓰면 타입 정보가 사라짐
function firstElementAny(arr: any[]): any {
return arr[0];
}
const result = firstElementAny([1, 2, 3]); // result: any 😢
// ✅ 제네릭: 타입 정보가 보존됨
function firstElement<T>(arr: T[]): T | undefined {
return arr[0];
}
const num = firstElement([1, 2, 3]); // num: number | undefined
const str = firstElement(["a", "b"]); // str: string | undefined
const explicit = firstElement<boolean>([true]); // 명시적 지정도 가능
8.2 제네릭 인터페이스와 타입
// API 응답 구조를 제네릭으로 추상화
interface ApiResponse<T> {
success: boolean;
data: T;
error?: string;
timestamp: number;
}
interface User { id: number; name: string; }
interface Product { id: number; title: string; price: number; }
// 같은 구조, 다른 데이터
type UserResponse = ApiResponse<User>;
type ProductListResponse = ApiResponse<Product[]>;
// 여러 타입 매개변수
interface KeyValuePair<K, V> {
key: K;
value: V;
}
const pair: KeyValuePair<string, number> = { key: "age", value: 28 };
8.3 제네릭 제약 조건 (Constraints)
제네릭 타입 매개변수에 제약을 걸어, 특정 구조를 가진 타입만 허용할 수 있습니다. extends 키워드를 사용합니다.
// T는 반드시 length 속성을 가져야 함
interface HasLength {
length: number;
}
function logLength<T extends HasLength>(item: T): T {
console.log(`Length: ${item.length}`);
return item;
}
logLength("hello"); // ✅ string은 length가 있음
logLength([1, 2, 3]); // ✅ 배열도 length가 있음
logLength(42); // ❌ number에는 length가 없음
// keyof와 함께 사용: 객체의 키를 안전하게 접근
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key];
}
const user = { name: "Alice", age: 30 };
getProperty(user, "name"); // ✅ 반환 타입: string
getProperty(user, "age"); // ✅ 반환 타입: number
getProperty(user, "email"); // ❌ 'email'은 'name' | 'age'에 없음
8.4 제네릭 기본값
// 타입 매개변수에 기본값 지정
interface Container<T = string> {
value: T;
}
const a: Container = { value: "hello" }; // T = string (기본값)
const b: Container<number> = { value: 42 }; // T = number (명시)
TResult, TInput처럼 의미를 담은 이름을 쓰는 것이 가독성에 좋습니다.
클래스 (Class)
9.1 TypeScript 클래스의 특징
TypeScript의 클래스는 JavaScript ES6 클래스에 접근 제어자(public, private, protected), 추상 클래스, 인터페이스 구현 등의 기능을 추가합니다. 이런 기능들은 컴파일 시 제거되지만, 개발 중에 코드 구조와 의도를 명확히 표현해 줍니다.
class User {
// 속성 선언과 접근 제어자
public name: string; // 어디서든 접근 가능 (기본값)
private password: string; // 클래스 내부에서만 접근
protected email: string; // 클래스 내부 + 자식 클래스에서 접근
readonly id: number; // 생성 후 변경 불가
constructor(id: number, name: string, email: string, password: string) {
this.id = id;
this.name = name;
this.email = email;
this.password = password;
}
// 메서드
greet(): string {
return `안녕하세요, ${this.name}입니다.`;
}
}
// 생성자 매개변수 축약 문법 (매우 자주 사용!)
class Product {
constructor(
public readonly id: number,
public name: string,
public price: number,
private stock: number = 0
) {}
// 위 코드만으로 속성 선언 + 할당이 모두 끝남!
}
9.2 인터페이스 구현 (implements)
interface Printable {
print(): void;
}
interface Savable {
save(): Promise<void>;
}
// 클래스가 여러 인터페이스를 구현
class Document implements Printable, Savable {
constructor(public content: string) {}
print(): void {
console.log(this.content);
}
async save(): Promise<void> {
// 저장 로직
console.log("저장 완료");
}
}
9.3 추상 클래스 (Abstract Class)
추상 클래스는 직접 인스턴스를 생성할 수 없고, 반드시 상속받아 구현해야 합니다. 인터페이스와 달리 구현된 메서드와 추상 메서드를 모두 가질 수 있습니다.
abstract class Shape {
abstract getArea(): number; // 자식이 반드시 구현
abstract describe(): string; // 자식이 반드시 구현
// 구현된 메서드: 모든 자식이 공유
displayArea(): void {
console.log(`면적: ${this.getArea()}`);
}
}
class Circle extends Shape {
constructor(private radius: number) { super(); }
getArea(): number { return Math.PI * this.radius ** 2; }
describe(): string { return `반지름 ${this.radius}인 원`; }
}
// const shape = new Shape(); // ❌ 추상 클래스는 직접 인스턴스 생성 불가
const circle = new Circle(5);
circle.displayArea(); // "면적: 78.53981633974483"
열거형 (Enum)
10.1 숫자 열거형
Enum은 관련된 상수들의 집합에 이름을 붙입니다. JavaScript에는 없는 TypeScript 고유 기능입니다. 숫자 열거형은 자동으로 0부터 증가하는 값이 할당됩니다.
enum Direction {
Up, // 0
Down, // 1
Left, // 2
Right // 3
}
enum HttpStatus {
OK = 200,
NotFound = 404,
InternalError = 500
}
function handleResponse(status: HttpStatus) {
if (status === HttpStatus.OK) {
console.log("성공!");
}
}
10.2 문자열 열거형
문자열 열거형은 디버깅이 쉽고 직렬화 시 의미가 명확합니다. 실무에서는 숫자 열거형보다 많이 사용됩니다.
enum OrderStatus {
Pending = "PENDING",
Processing = "PROCESSING",
Shipped = "SHIPPED",
Delivered = "DELIVERED",
Cancelled = "CANCELLED"
}
const order = {
id: 1,
status: OrderStatus.Pending
};
console.log(order.status); // "PENDING"
10.3 const enum
const enum은 컴파일 시 완전히 인라인됩니다. 일반 enum은 JavaScript 객체로 변환되지만, const enum은 사용된 곳에 값이 직접 삽입되어 번들 크기가 줄어듭니다.
const enum Color {
Red = "#FF0000",
Green = "#00FF00",
Blue = "#0000FF"
}
const bg = Color.Red;
// 컴파일 결과: const bg = "#FF0000";
// (enum 객체가 생성되지 않고, 값이 직접 대체됨)
10.4 Enum 대안: 리터럴 유니온
많은 TypeScript 개발자들은 enum 대신 리터럴 유니온 타입과 as const를 선호합니다. 더 가볍고, 트리셰이킹에 유리하며, JavaScript와의 호환성이 좋기 때문입니다.
// 방법 1: 리터럴 유니온 타입
type Direction = "up" | "down" | "left" | "right";
// 방법 2: as const 객체 (값과 타입을 모두 사용 가능)
const STATUS = {
Pending: "PENDING",
Active: "ACTIVE",
Closed: "CLOSED",
} as const;
type Status = typeof STATUS[keyof typeof STATUS];
// 결과: "PENDING" | "ACTIVE" | "CLOSED"
타입 좁히기 (Type Narrowing)
11.1 타입 좁히기란?
TypeScript의 타입 좁히기는 넓은 타입에서 좁은 타입으로 정제하는 과정입니다. 유니온 타입 string | number를 가진 변수가 있을 때, 조건문을 통해 특정 분기에서 string 또는 number임을 확정하는 것입니다. TypeScript의 컴파일러는 제어 흐름 분석(Control Flow Analysis)을 통해 이를 자동으로 수행합니다.
11.2 다양한 좁히기 기법
// 1. typeof 가드
function process(value: string | number) {
if (typeof value === "string") {
value.toUpperCase(); // ✅ value: string
} else {
value.toFixed(2); // ✅ value: number
}
}
// 2. instanceof 가드
function formatDate(input: Date | string) {
if (input instanceof Date) {
return input.toISOString(); // ✅ input: Date
}
return new Date(input).toISOString(); // input: string
}
// 3. in 연산자 가드
interface Bird { fly(): void; layEggs(): void; }
interface Fish { swim(): void; layEggs(): void; }
function move(animal: Bird | Fish) {
if ("fly" in animal) {
animal.fly(); // ✅ animal: Bird
} else {
animal.swim(); // ✅ animal: Fish
}
}
// 4. Truthiness 가드 (null/undefined 체크)
function print(value: string | null | undefined) {
if (value) {
value.toUpperCase(); // ✅ value: string (null과 undefined 제외됨)
}
}
11.3 사용자 정의 타입 가드
내장 가드로 충분하지 않을 때, 직접 타입 가드 함수를 만들 수 있습니다. 반환 타입에 is 키워드를 사용합니다.
interface Cat { meow(): void; purr(): void; }
interface Dog { bark(): void; fetch(): void; }
// 사용자 정의 타입 가드: 반환 타입이 "animal is Cat"
function isCat(animal: Cat | Dog): animal is Cat {
return "meow" in animal;
}
function handlePet(pet: Cat | Dog) {
if (isCat(pet)) {
pet.purr(); // ✅ pet: Cat
} else {
pet.fetch(); // ✅ pet: Dog
}
}
// 실전 예시: API 응답 검증
interface SuccessResponse { status: "ok"; data: unknown; }
interface ErrorResponse { status: "error"; message: string; }
function isSuccess(res: SuccessResponse | ErrorResponse): res is SuccessResponse {
return res.status === "ok";
}
11.4 exhaustiveness 검사 (never를 이용한 완전성 검사)
switch문에서 모든 케이스를 처리했는지 컴파일러가 보장하게 만들 수 있습니다. 처리되지 않은 케이스가 있으면 컴파일 에러가 발생합니다.
type Shape = Circle | Rectangle | Triangle;
function getArea(shape: Shape): number {
switch (shape.kind) {
case "circle": return Math.PI * shape.radius ** 2;
case "rectangle": return shape.width * shape.height;
case "triangle": return (shape.base * shape.height) / 2;
default: {
// 모든 케이스를 처리했다면 여기 도달할 수 없으므로 shape은 never
const _exhaustive: never = shape;
return _exhaustive;
// 새로운 Shape 타입이 추가되면 여기서 컴파일 에러 발생!
}
}
}
유틸리티 타입 (Utility Types)
12.1 유틸리티 타입이란?
TypeScript는 기존 타입을 변환하기 위한 내장 유틸리티 타입을 제공합니다. 이 타입들은 실무에서 매우 자주 사용되며, 복잡한 타입 조작을 간결하게 표현합니다. 이미 존재하는 타입에서 새로운 타입을 파생시키는 것이 핵심 아이디어입니다.
12.2 객체 변환 유틸리티
interface User {
id: number;
name: string;
email: string;
age: number;
}
// Partial<T> — 모든 속성을 선택적으로
type UpdateUserDto = Partial<User>;
// { id?: number; name?: string; email?: string; age?: number; }
// Required<T> — 모든 속성을 필수로
type StrictUser = Required<User>;
// Readonly<T> — 모든 속성을 읽기 전용으로
type FrozenUser = Readonly<User>;
// 모든 속성 수정 불가
// Pick<T, K> — 특정 속성만 선택
type UserPreview = Pick<User, "id" | "name">;
// { id: number; name: string; }
// Omit<T, K> — 특정 속성을 제외
type CreateUserDto = Omit<User, "id">;
// { name: string; email: string; age: number; }
// Record<K, V> — 키-값 쌍의 타입 정의
type UserRoles = Record<string, "admin" | "user" | "guest">;
// { [key: string]: "admin" | "user" | "guest" }
12.3 유니온 변환 유틸리티
type AllStatuses = "active" | "inactive" | "pending" | "deleted";
// Exclude<T, U> — T에서 U에 해당하는 것 제외
type ActiveStatuses = Exclude<AllStatuses, "deleted" | "inactive">;
// "active" | "pending"
// Extract<T, U> — T에서 U에 해당하는 것만 추출
type DeadStatuses = Extract<AllStatuses, "inactive" | "deleted">;
// "inactive" | "deleted"
// NonNullable<T> — null과 undefined 제거
type MaybeString = string | null | undefined;
type DefiniteString = NonNullable<MaybeString>;
// string
12.4 함수 관련 유틸리티
function createUser(name: string, age: number, email: string): User {
return { id: Date.now(), name, age, email };
}
// ReturnType<T> — 함수의 반환 타입 추출
type NewUser = ReturnType<typeof createUser>;
// User
// Parameters<T> — 함수의 매개변수 타입을 튜플로 추출
type CreateUserParams = Parameters<typeof createUser>;
// [name: string, age: number, email: string]
// Awaited<T> — Promise의 resolved 타입 추출 (TypeScript 4.5+)
type ApiData = Awaited<Promise<User[]>>;
// User[]
12.5 실전 활용
// 실전: API 엔드포인트별 요청/응답 타입 관리
interface User {
id: number;
name: string;
email: string;
role: "admin" | "user";
createdAt: Date;
}
// 생성 시: id와 createdAt은 서버에서 생성
type CreateUserRequest = Omit<User, "id" | "createdAt">;
// 수정 시: 모든 필드 선택적, 단 id는 필수
type UpdateUserRequest = Partial<CreateUserRequest> & Pick<User, "id">;
// 목록 응답: 민감 정보 제외
type UserListItem = Pick<User, "id" | "name" | "role">;
고급 타입 (Advanced Types)
13.1 조건부 타입 (Conditional Types)
타입 수준에서의 삼항 연산자입니다. T extends U ? X : Y 형태로, 조건에 따라 다른 타입을 반환합니다. 제네릭과 결합하면 매우 강력한 타입 변환이 가능합니다.
// 기본 조건부 타입
type IsString<T> = T extends string ? "yes" : "no";
type A = IsString<string>; // "yes"
type B = IsString<number>; // "no"
// 실전: 배열이면 요소 타입 추출, 아니면 그대로
type Flatten<T> = T extends (infer Item)[] ? Item : T;
type Str = Flatten<string[]>; // string
type Num = Flatten<number>; // number (배열이 아님)
13.2 매핑된 타입 (Mapped Types)
기존 타입의 각 속성을 변환하여 새 타입을 만듭니다. 실제로 Partial, Readonly, Required 등의 유틸리티 타입이 내부적으로 이 기법을 사용합니다.
// Partial<T>의 실제 구현
type MyPartial<T> = {
[K in keyof T]?: T[K];
};
// Readonly<T>의 실제 구현
type MyReadonly<T> = {
readonly [K in keyof T]: T[K];
};
// 커스텀: 모든 속성을 nullable로 변환
type Nullable<T> = {
[K in keyof T]: T[K] | null;
};
// 커스텀: 모든 속성을 getter 함수로 변환
type Getters<T> = {
[K in keyof T as `get${Capitalize<string & K>}`]: () => T[K];
};
interface Person { name: string; age: number; }
type PersonGetters = Getters<Person>;
// { getName: () => string; getAge: () => number; }
13.3 템플릿 리터럴 타입 (Template Literal Types)
문자열 리터럴 타입을 템플릿으로 조합하여 새로운 문자열 리터럴 타입을 만듭니다.
type Color = "red" | "blue" | "green";
type Size = "sm" | "md" | "lg";
// 모든 조합 자동 생성!
type ClassName = `${Color}-${Size}`;
// "red-sm" | "red-md" | "red-lg" | "blue-sm" | "blue-md" | "blue-lg" | ...
// CSS 속성 이름 패턴
type CSSProperty = `${"margin" | "padding"}-${"top" | "right" | "bottom" | "left"}`;
// "margin-top" | "margin-right" | ... | "padding-left"
// 이벤트 핸들러 이름 패턴
type EventName = "click" | "focus" | "blur";
type Handler = `on${Capitalize<EventName>}`;
// "onClick" | "onFocus" | "onBlur"
13.4 infer 키워드
infer는 조건부 타입 내에서 타입 변수를 선언하고, 패턴 매칭으로 타입을 추출합니다. TypeScript 타입 시스템의 가장 고급 기능 중 하나입니다.
// Promise의 내부 타입 추출
type UnwrapPromise<T> = T extends Promise<infer U> ? U : T;
type A = UnwrapPromise<Promise<string>>; // string
type B = UnwrapPromise<number>; // number
// 함수의 첫 번째 매개변수 타입 추출
type FirstArg<T> = T extends (first: infer F, ...args: any[]) => any ? F : never;
type C = FirstArg<(name: string, age: number) => void>; // string
모듈 시스템
14.1 ES Modules (ESM)
TypeScript는 JavaScript의 표준 모듈 시스템인 ES Modules를 사용합니다. import와 export로 모듈 간에 코드를 공유합니다. 모든 최신 브라우저와 Node.js가 ESM을 지원하며, TypeScript 6.0에서는 ESM이 기본 모듈 형식이 되었습니다.
// Named Export (이름 있는 내보내기)
export function add(a: number, b: number): number {
return a + b;
}
export function subtract(a: number, b: number): number {
return a - b;
}
export const PI = 3.14159;
// Default Export (기본 내보내기) — 모듈당 하나
export default class Calculator {
evaluate(expr: string): number { /* ... */ return 0; }
}
// Named Import
import { add, subtract, PI } from "./math.js";
// Default Import
import Calculator from "./math.js";
// 별칭 지정
import { add as sum } from "./math.js";
// 전체 가져오기
import * as MathUtils from "./math.js";
// 타입만 가져오기 (컴파일 후 완전히 제거됨)
import type { User } from "./types.js";
14.2 import type
import type은 TypeScript 3.8에서 도입된 중요한 기능입니다. 타입 정보만 가져오고, 컴파일 후 JavaScript에는 아무런 흔적도 남기지 않습니다. 번들 크기를 줄이고, 순환 의존성 문제를 방지하는 데 도움됩니다. 최신 TypeScript에서는 verbatimModuleSyntax 옵션을 켜면 타입 전용 임포트에 import type을 강제할 수 있습니다.
14.3 모듈 해석 전략
TypeScript의 moduleResolution 옵션은 import 경로를 어떻게 해석할지 결정합니다. TypeScript 6.0에서는 node10(구 node)과 classic 전략이 폐기되었습니다. 현재 권장되는 설정은 Node.js 직접 실행 시 nodenext, 번들러 사용 시 bundler입니다.
데코레이터 (Decorators)
15.1 데코레이터란?
데코레이터는 클래스, 메서드, 속성, 접근자 등에 추가 동작을 선언적으로 부여하는 기능입니다. @expression 형태로 사용하며, ECMAScript 표준 데코레이터가 TC39 Stage 3를 거쳐 TypeScript 5.0부터 공식 지원됩니다. Angular, NestJS 등 많은 프레임워크에서 핵심적으로 사용됩니다.
// 메서드 데코레이터: 실행 시간 측정
function log(
target: any,
context: ClassMethodDecoratorContext
) {
const methodName = String(context.name);
return function (this: any, ...args: any[]) {
console.log(`→ ${methodName} 호출, 인자:`, args);
const start = performance.now();
const result = target.call(this, ...args);
const end = performance.now();
console.log(`← ${methodName} 완료 (${(end - start).toFixed(2)}ms)`);
return result;
};
}
class MathService {
@log
factorial(n: number): number {
return n <= 1 ? 1 : n * this.factorial(n - 1);
}
}
const math = new MathService();
math.factorial(10);
// → factorial 호출, 인자: [10]
// ← factorial 완료 (0.05ms)
15.2 클래스 데코레이터
// 클래스에 메타데이터 추가
function sealed(constructor: Function, context: ClassDecoratorContext) {
Object.seal(constructor);
Object.seal(constructor.prototype);
}
@sealed
class BankAccount {
constructor(public owner: string, private balance: number) {}
deposit(amount: number) { this.balance += amount; }
getBalance() { return this.balance; }
}
experimentalDecorators 옵션 기반과, TC39 표준 기반(5.0+)입니다. 새 프로젝트에서는 표준 데코레이터를 사용하세요. Angular 등 일부 프레임워크는 아직 experimental 옵션을 사용할 수 있습니다.
타입 선언 파일 (.d.ts)
16.1 선언 파일이란?
.d.ts 파일은 타입 정보만 담고 있는 파일입니다. 실행 가능한 코드(구현)는 없고, 타입 시그니처만 선언합니다. JavaScript로 작성된 라이브러리에 TypeScript 타입 정보를 제공하는 것이 주된 용도입니다.
npm에서 설치하는 패키지가 TypeScript로 작성되지 않았더라도, @types/패키지명으로 커뮤니티가 만든 타입 선언을 설치할 수 있습니다. DefinitelyTyped 프로젝트에서 관리하는 이 저장소에는 12,000개 이상의 패키지 타입이 등록되어 있습니다.
# JavaScript 라이브러리의 타입 설치
npm install -D @types/node # Node.js API 타입
npm install -D @types/react # React 타입
npm install -D @types/lodash # Lodash 타입
# 최근의 라이브러리는 타입이 내장되어 있어 별도 설치 불필요
# (package.json에 "types" 필드가 있음)
npm install zod # 타입 내장
npm install axios # 타입 내장
16.2 직접 선언 파일 작성하기
// 전역 변수 선언 (window에 추가된 변수 등)
declare const APP_VERSION: string;
declare const __DEV__: boolean;
// 외부 모듈 선언 (타입이 없는 라이브러리)
declare module "some-untyped-lib" {
export function doSomething(input: string): number;
export default class Client {
constructor(options: { apiKey: string });
fetch(url: string): Promise<any>;
}
}
// 파일 모듈 선언 (이미지, CSS 등 비-JS 파일)
declare module "*.png" {
const src: string;
export default src;
}
declare module "*.css" {
const classes: { readonly [key: string]: string };
export default classes;
}
tsconfig.json 완전 가이드
17.1 tsconfig.json이란?
tsconfig.json은 TypeScript 프로젝트의 설정 파일입니다. 어떤 파일을 컴파일할지, 어떤 규칙으로 타입을 검사할지, 결과물을 어디에 어떤 형식으로 생성할지 등을 정의합니다. 프로젝트 루트에 위치하며, npx tsc --init으로 자동 생성할 수 있습니다.
17.2 실전 권장 설정
{
"compilerOptions": {
// ─── 기본 설정 ───
"target": "ES2022", // 출력 JavaScript 버전
"module": "nodenext", // 모듈 시스템
"moduleResolution": "nodenext", // 모듈 해석 전략
"lib": ["ES2022", "DOM"], // 사용할 내장 타입 라이브러리
// ─── 출력 설정 ───
"outDir": "./dist", // 컴파일 결과 디렉터리
"rootDir": "./src", // 소스 루트 디렉터리
"declaration": true, // .d.ts 파일 생성
"sourceMap": true, // 디버깅용 소스맵 생성
// ─── 엄격한 타입 검사 (모두 켜는 것을 강력 추천) ───
"strict": true, // 아래 모든 strict 옵션을 한번에 활성화
// "noImplicitAny": true, — 암묵적 any 금지
// "strictNullChecks": true, — null/undefined 엄격 검사
// "strictFunctionTypes": true, — 함수 타입 엄격 검사
// "strictBindCallApply": true, — bind/call/apply 엄격 검사
// "noImplicitThis": true, — 암묵적 this 금지
// "alwaysStrict": true, — 항상 strict mode
// ─── 추가 검사 ───
"noUnusedLocals": true, // 사용하지 않는 지역 변수 에러
"noUnusedParameters": true, // 사용하지 않는 매개변수 에러
"noFallthroughCasesInSwitch": true, // switch fallthrough 에러
"forceConsistentCasingInFileNames": true, // 파일명 대소문자 일관성
// ─── 호환성 ───
"esModuleInterop": true, // CommonJS/ESM 상호 운용
"skipLibCheck": true, // .d.ts 타입 검사 건너뛰기 (속도↑)
"resolveJsonModule": true, // JSON import 허용
// ─── 타입 패키지 ───
"types": ["node"] // 6.0부터 명시적 지정 권장
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist"]
}
17.3 핵심 옵션 빠른 설명
| 옵션 | 역할 | 권장값 |
|---|---|---|
strict | 모든 엄격 검사 활성화 | true |
target | 출력 JS 버전 | "ES2022" 이상 |
module | 모듈 시스템 | "nodenext" / "esnext" |
moduleResolution | import 경로 해석 방식 | "nodenext" / "bundler" |
outDir | 컴파일 결과 디렉터리 | "./dist" |
declaration | .d.ts 생성 여부 | 라이브러리: true |
skipLibCheck | 외부 .d.ts 검사 건너뛰기 | true (빌드 속도) |
strict, module: "esnext", target: "es2025"가 기본값이 되었고, types가 빈 배열 []로 기본 설정됩니다. 기존 프로젝트를 업그레이드할 때 "types": ["node"] 등을 명시적으로 추가해야 합니다.
실전 패턴 & 베스트 프랙티스
18.1 API 응답 타입 안전하게 다루기
// 제네릭 API 래퍼
interface ApiSuccess<T> {
ok: true;
data: T;
}
interface ApiError {
ok: false;
error: {
code: string;
message: string;
};
}
type ApiResult<T> = ApiSuccess<T> | ApiError;
// 타입 안전한 fetch 래퍼
async function fetchApi<T>(url: string): Promise<ApiResult<T>> {
try {
const response = await fetch(url);
if (!response.ok) {
return { ok: false, error: { code: "HTTP_ERROR", message: response.statusText } };
}
const data: T = await response.json();
return { ok: true, data };
} catch (e) {
return { ok: false, error: { code: "NETWORK_ERROR", message: String(e) } };
}
}
// 사용: 판별 유니온으로 안전하게 처리
interface User { id: number; name: string; }
const result = await fetchApi<User[]>("/api/users");
if (result.ok) {
result.data.forEach(user => console.log(user.name)); // ✅ data: User[]
} else {
console.error(result.error.message); // ✅ error 접근 가능
}
18.2 Builder 패턴
class QueryBuilder<T> {
private filters: string[] = [];
private sortField?: keyof T;
private limitCount?: number;
where(field: keyof T, op: "=" | "!=" | ">" | "<", value: unknown): this {
this.filters.push(`${String(field)} ${op} ${value}`);
return this;
}
orderBy(field: keyof T): this {
this.sortField = field;
return this;
}
limit(n: number): this {
this.limitCount = n;
return this;
}
build(): string {
let query = "SELECT * FROM table";
if (this.filters.length) query += ` WHERE ${this.filters.join(" AND ")}`;
if (this.sortField) query += ` ORDER BY ${String(this.sortField)}`;
if (this.limitCount) query += ` LIMIT ${this.limitCount}`;
return query;
}
}
interface Product { id: number; name: string; price: number; }
const query = new QueryBuilder<Product>()
.where("price", ">", 1000) // ✅ "price"는 Product의 키
.where("name", "!=", "test") // ✅
.orderBy("price") // ✅
.limit(10)
.build();
// "SELECT * FROM table WHERE price > 1000 AND name != test ORDER BY price LIMIT 10"
18.3 타입 안전한 이벤트 이미터
// 이벤트 맵을 제네릭으로 정의
type EventMap = {
login: { userId: string; timestamp: Date };
logout: { userId: string };
purchase: { productId: number; amount: number };
};
class TypedEmitter<Events extends Record<string, any>> {
private handlers = new Map<string, Set<Function>>();
on<K extends keyof Events>(event: K, handler: (data: Events[K]) => void): void {
const key = event as string;
if (!this.handlers.has(key)) this.handlers.set(key, new Set());
this.handlers.get(key)!.add(handler);
}
emit<K extends keyof Events>(event: K, data: Events[K]): void {
this.handlers.get(event as string)?.forEach(h => h(data));
}
}
const emitter = new TypedEmitter<EventMap>();
emitter.on("login", (data) => {
console.log(data.userId); // ✅ 자동완성: userId, timestamp
});
emitter.emit("purchase", {
productId: 42,
amount: 29900 // ✅ 타입 검사됨
});
emitter.emit("login", { wrong: true }); // ❌ 타입 에러!
18.4 자주 사용하는 베스트 프랙티스
1.
any 대신 unknown을 쓰세요. 타입을 모를 때는 unknown을 쓰고, 사용하기 전에 타입을 좁히세요.2.
as 타입 단언을 최소화하세요. 타입 단언은 컴파일러의 검사를 우회합니다. 타입 가드나 제네릭으로 대체 가능한지 먼저 확인하세요.3.
strict: true를 반드시 활성화하세요. 엄격 모드가 꺼져 있으면 TypeScript를 쓰는 의미가 반감됩니다.4. 반환 타입을 명시하세요. 특히 공개 API(export되는 함수)는 반환 타입을 명시적으로 적는 것이 좋습니다. 타입 추론에만 의존하면 의도치 않은 타입 변경이 발생할 수 있습니다.
5.
enum보다 as const를 고려하세요. const 객체 + typeof로 같은 효과를 얻으면서 트리셰이킹에 유리합니다.
TypeScript 6.0 & 7.0 — 미래를 향해
19.1 TypeScript 6.0 (2026년 3월 출시)
TypeScript 6.0은 현재 JavaScript 기반 코드베이스의 마지막 릴리스이며, Go 언어로 작성된 TypeScript 7.0으로의 전환을 준비하는 "다리" 역할을 합니다. 기능적 변화보다는 기본값 변경과 폐기(deprecation) 중심의 릴리스입니다.
주요 기본값 변경
strict가 기본으로 true가 되었고, module의 기본값이 "esnext"로, target의 기본값이 최신 ES 버전(현재 es2025)으로 변경되었습니다. 또한 types의 기본값이 빈 배열 []로 바뀌어, @types/node 등을 명시적으로 지정해야 합니다. rootDir도 기존의 추론 방식 대신 tsconfig.json이 위치한 디렉터리(".")가 기본값입니다.
폐기된 옵션들
target: "es5", moduleResolution: "node"(node10), moduleResolution: "classic", module: "amd"/"umd"/"systemjs", outFile, baseUrl 등 레거시 옵션들이 폐기되었습니다. "ignoreDeprecations": "6.0"으로 일시적으로 무시할 수 있지만, 7.0에서는 완전히 제거됩니다.
새로운 기능
ECMAScript Temporal API의 내장 타입 지원, RegExp.escape, Map.getOrInsert/getOrInsertComputed 등 ES2025 표준의 새 API 타입이 추가되었습니다. #/ 접두사 서브패스 임포트(Node.js 20+)도 지원됩니다.
19.2 TypeScript 7.0 — Go 네이티브 포트
TypeScript 7.0은 TypeScript 역사상 가장 큰 변화입니다. 컴파일러가 JavaScript(tsc)에서 Go 언어로 다시 작성됩니다. 네이티브 코드의 속도와 Go의 공유 메모리 멀티스레딩을 활용하여, 대규모 프로젝트에서 10배 이상의 빌드 속도 향상이 예상됩니다.
병렬 타입 체킹이 도입되어 여러 파일을 동시에 검사할 수 있게 됩니다. 이는 극적인 성능 향상을 가져오지만, 내부적으로 타입 ID 순서가 비결정적이 될 수 있어서, TypeScript 6.0에서 --stableTypeOrdering 플래그를 제공하여 사전 마이그레이션을 돕고 있습니다.
# TypeScript 7.0 네이티브 프리뷰 설치 (npm)
npm install -D @typescript/native-preview
# VS Code 확장 설치로 에디터에서 바로 체험 가능
19.3 마이그레이션 체크리스트
1.
"types": ["node"] (또는 필요한 패키지) 명시 추가2.
"rootDir": "./src" 명시적 설정3.
baseUrl을 사용하던 경우, 제거하고 paths에 접두사 추가4.
target: "es5"를 사용하던 경우, "es2015" 이상으로 변경5.
moduleResolution: "node"를 "nodenext" 또는 "bundler"로 변경6. import assertion(
assert)을 import attribute(with)로 변경7.
ts5to6 마이그레이션 도구 활용 고려
부록 — TypeScript 치트시트 & 학습 로드맵
20.1 타입 시스템 치트시트
/* =============================================
TypeScript 치트시트 - 한눈에 보는 타입 시스템
============================================= */
/* ── 1. 기본 타입 ── */
let str: string = "hello";
let num: number = 42;
let bool: boolean = true;
let nul: null = null;
let undef: undefined = undefined;
let big: bigint = 100n;
let sym: symbol = Symbol("id");
/* ── 2. 특수 타입 ── */
let anything: any; // 타입 검사 비활성화 (사용 지양)
let safe: unknown; // 안전한 any (사용 전 타입 확인 필수)
function fail(): never { throw new Error(); } // 반환 없음
function log(): void { console.log("hi"); } // 반환값 무시
/* ── 3. 배열과 튜플 ── */
let arr: number[] = [1, 2, 3];
let arr2: Array<string> = ["a", "b"];
let tuple: [string, number] = ["age", 28];
let rest: [string, ...number[]] = ["scores", 90, 85];
/* ── 4. 객체 타입 ── */
interface User {
id: number;
name: string;
email?: string; // 선택적 속성
readonly createdAt: Date; // 읽기 전용
}
type Point = { x: number; y: number };
/* ── 5. 유니온 & 인터섹션 ── */
type ID = string | number; // 유니온: 둘 중 하나
type Named = { name: string };
type Aged = { age: number };
type Person = Named & Aged; // 인터섹션: 둘 다 합침
/* ── 6. 리터럴 & as const ── */
type Dir = "up" | "down" | "left" | "right";
const CONFIG = { api: "/v1", timeout: 3000 } as const;
// typeof CONFIG = { readonly api: "/v1"; readonly timeout: 3000 }
/* ── 7. 제네릭 ── */
function identity<T>(value: T): T { return value; }
interface Box<T> { content: T; }
type Pair<A, B> = { first: A; second: B };
/* ── 8. 유틸리티 타입 ── */
Partial<T> // 모든 속성 선택적
Required<T> // 모든 속성 필수
Readonly<T> // 모든 속성 읽기전용
Pick<T, K> // 특정 속성만 선택
Omit<T, K> // 특정 속성 제외
Record<K, V> // 키-값 매핑 타입
Exclude<T, U> // T에서 U 제외
Extract<T, U> // T에서 U만 추출
NonNullable<T> // null | undefined 제거
ReturnType<T> // 함수 반환 타입
Parameters<T> // 함수 매개변수 타입 (튜플)
Awaited<T> // Promise 내부 타입
/* ── 9. 타입 가드 ── */
typeof x === "string" // 원시 타입 체크
x instanceof Date // 클래스 인스턴스 체크
"key" in obj // 속성 존재 체크
function isFoo(x: any): x is Foo { ... } // 커스텀 가드
/* ── 10. 조건부 타입 ── */
type IsStr<T> = T extends string ? "yes" : "no";
type Flat<T> = T extends (infer U)[] ? U : T;
/* ── 11. 매핑된 타입 ── */
type Nullable<T> = { [K in keyof T]: T[K] | null };
type Getters<T> = { [K in keyof T as `get${Capitalize<string & K>}`]: () => T[K] };
/* ── 12. 템플릿 리터럴 타입 ── */
type EventName = `on${Capitalize<"click" | "focus">}`;
// "onClick" | "onFocus"
/* ── 13. 클래스 ── */
class Foo {
public x: number; // 어디서든 접근
private y: number; // 클래스 내부만
protected z: number; // 내부 + 자식
readonly w: number; // 수정 불가
constructor(public name: string) {} // 축약 선언
}
abstract class Base { abstract method(): void; }
/* ── 14. 모듈 ── */
export { foo };
export default class Bar {}
import { foo } from "./mod.js";
import type { MyType } from "./types.js";
20.2 자주 하는 실수 & 해결
| 실수 | 증상 | 해결 |
|---|---|---|
any 남용 | 타입 검사 무력화, 런타임 에러 빈발 | unknown + 타입 가드 사용 |
as 타입 단언 남용 | 컴파일러를 속임, 런타임 에러 가능 | 제네릭이나 타입 가드로 대체 |
| 선택적 속성에 바로 접근 | Cannot read properties of undefined | 옵셔널 체이닝 ?. 또는 null 체크 |
=== 대신 == 사용 | 의도치 않은 타입 변환 비교 | 항상 === 사용 (ESLint eqeqeq 규칙) |
Object, String, Number 래퍼 타입 사용 | 원시 타입과 호환 안 됨 | 소문자 object, string, number 사용 |
enum에 숫자값 사용 | 역방향 매핑으로 예기치 않은 동작 | 문자열 enum 또는 as const 객체 사용 |
인터페이스에 | 유니온 사용 시도 | 문법 에러 | 유니온은 type으로만 정의 가능 |
import 경로에 .ts 확장자 | 모듈 해석 에러 (nodenext) | .js 확장자 사용 (TS가 알아서 .ts 파일을 찾음) |
Promise 반환 함수에 await 누락 | Promise 객체 자체가 반환됨 | @typescript-eslint/no-floating-promises 규칙 활성화 |
| 제네릭 기본값 없이 복잡한 타입 조합 | 사용처에서 장황한 타입 지정 필요 | 제네릭에 = defaultType 기본값 지정 |
20.3 학습 로드맵
| 단계 | 기간 | 학습 내용 | 이 튜토리얼 |
|---|---|---|---|
| Level 1 입문 | 1~2주 | 기본 타입, 함수 타입, 인터페이스, 배열/튜플 | Ch.1 ~ Ch.6 |
| Level 2 기초 | 2~3주 | 유니온/인터섹션, 타입 별칭, 제네릭, 클래스, Enum | Ch.7 ~ Ch.10 |
| Level 3 중급 | 1~2개월 | 타입 좁히기, 유틸리티 타입, 고급 타입, 모듈, 데코레이터 | Ch.11 ~ Ch.16 |
| Level 4 실전 | 1~2개월 | tsconfig 최적화, 실전 패턴, 라이브러리 타입 작성 | Ch.17 ~ Ch.18 |
| Level 5 전문가 | 지속 | 조건부 타입 심화, 타입 수준 프로그래밍, 성능 최적화, 기여 | 공식 문서 + 실무 |
20.4 추천 학습 자료
TypeScript를 더 깊이 학습하고 싶다면 다음 자료를 추천합니다. 공식 TypeScript Handbook(typescriptlang.org/docs/handbook)은 가장 신뢰할 수 있는 레퍼런스입니다. Matt Pocock의 "Total TypeScript" 시리즈는 고급 타입 패턴을 실전 예제로 설명합니다. type-challenges(GitHub)는 타입 수준 프로그래밍을 퍼즐처럼 연습할 수 있는 프로젝트로, 제네릭과 조건부 타입 실력을 크게 끌어올릴 수 있습니다. TypeScript 공식 블로그(devblogs.microsoft.com/typescript)에서는 매 릴리스의 새 기능을 상세히 소개합니다.
20.5 마치며
축하합니다! 전 20장의 "TypeScript 모든 것" 튜토리얼을 모두 마치셨습니다.
TypeScript는 단순히 JavaScript에 타입을 붙이는 도구가 아닙니다. 타입 시스템 자체가 하나의 프로그래밍 언어처럼 동작하며, 코드의 설계와 아키텍처를 근본적으로 바꿔 줍니다. 함수의 시그니처가 곧 문서가 되고, 인터페이스가 팀원 간의 계약서가 되며, 컴파일러가 24시간 코드 리뷰어 역할을 합니다.
TypeScript 7.0의 Go 네이티브 포트는 10배 이상의 빌드 속도를 약속하며, TypeScript 생태계의 새로운 장을 열고 있습니다. 지금이 TypeScript를 배우기에 가장 좋은 시기입니다.
1. strict 모드를 절대 끄지 마세요 — 엄격함이 불편하게 느껴질 수 있지만, 그 불편함이 바로 버그를 방지하는 안전망입니다. strict를 끄면 TypeScript를 쓰는 의미가 절반으로 줄어듭니다.
2. 타입을 데이터보다 먼저 설계하세요 — 코드를 작성하기 전에 인터페이스와 타입부터 정의하세요. 타입 설계가 곧 소프트웨어 설계입니다. "이 함수가 무엇을 받고 무엇을 반환하는가?"를 먼저 생각하면 더 좋은 코드가 나옵니다.
3. 에러 메시지를 끝까지 읽으세요 — TypeScript의 에러 메시지는 길고 복잡해 보이지만, 문제의 원인과 해결 방향을 매우 구체적으로 알려줍니다. "Type 'X' is not assignable to type 'Y'" 메시지를 차근히 읽으면, 타입 시스템에 대한 이해가 빠르게 깊어집니다.
타입의 세계에 오신 것을 환영합니다. 이 튜토리얼이 여러분의 TypeScript 여정에 든든한 첫걸음이 되기를 바랍니다!